Abstract Dependencies for Middleware in Haskell
Layer2をまず導入する
型クラスを使ってDSLの命令を定義する。
code:haskell
class Cache m where
writeCache :: (ToJSON a) => a -> m ()
class Persist m where
insert :: (ToTuple a) => a -> m ()
usecase :: (Cache m, Persist m) => m ()
usecase user = do
insert user >> writeCache user
Cache と Persist 2つのDSLを一つのモナド内でmixして使える、という魅力がある。
一般的なWeb DBシステムだと入力チェックと永続化、サブシステムへの通知とかがメインだからこれだけでこなせちゃう気もする。
問題点
例えば Cache と Persist をそれぞれ別のデータ型でインスタンス化すると考える。
それぞれのミドルウェア向けに個別に実装を与えたいから。
逆に God みたいなデータ型を用意して各種ドメインのセマンティクスをまとめて定義することもできる。
が、僕は Cache × Redis は一つのモジュールにして、 Persist × Sqlite は別のモジュールに実装を定義したかった。
code:haskell
newtype RedisClient a = RedisClient (ReaderT RedisConfig IO a)
instance Cache RedisClient where
...
data SqliteClient a = SqliteClient (ReaderT SqliteConfig IO a)
instance Persist SqliteClient where
...
とすると usecase の m を埋める適切なインスタンスがなくなる。
m を埋める単一の型が必要になる。そしてそれは、
code:haskell
newtype RedisSqlite = RS (RedisClient, SqliteClient)
みたいになるだろう。
これに Cache と Persist を実装するボイラープレートが面倒。
どちらの実装も Reader なので Handle パターンに集約する必要性も出てくる。
Clojureだと外部システムとの境界にいては
Freeモナドかなんかで合成する
code:haskell
-- | Functorの整備
data AppF a = Redis (RedisClient a) | Sqlite (SqliteClient a)
instance Functor AppF where
fmap f (Redis r) = Redis (fmap f r)
fmap f (Sqlite s) = Sqlite (fmap f s)
-- | Freeの整備
instance (Cache f) => Cache (Free f) where
writeCache = liftF . writeCache
instance (Persist f) => Persist (Free f) where
insert = liftF . insert
type AppM a = Free AppF a
希望(こうなっていて欲しい)
code:text
data A
data B
class F a where
instance F A where
があるとする
f :: A -> B
g :: B -> A
AとBが行き来できる ≒ 準同型であるなら
instance F B where
が導出できること。